Java 单例模式的安全性

单例模式

单例模式是指,某个对象在运行时仅存在一个,并对外提供统一访问方式。

  • 饿汉模式:类被加载的时候就立即初始化并创建唯一实例
  • 懒汉模式:在被首次调用的时候才创建唯一实例。加入双重检查锁机制能保证线程安全。

饿汉单例模式

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 饿汉单例模式
*/
public class BeanContainerStaving {

private static final BeanContainerStaving instance = new BeanContainerStaving();

// 禁止访问构造方法
private BeanContainerStaving()
{

}

public static BeanContainerStaving getInstance()
{
return instance;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SingletonTest {

public static void main(String[] args) {

Thread thread1 = new Thread()
{
@Override
public void run() {
System.out.println("Thread 1 get instance");
System.out.println(BeanContainerStaving.getInstance());
}
};

Thread thread2 = new Thread()
{
@Override
public void run() {
System.out.println("Thread 2 get instance");
System.out.println(BeanContainerStaving.getInstance());
}
};

thread2.start();
thread1.start();
}
}

结果

保证获取到的是唯一实例

image.png

懒汉单例模式

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 懒汉单例模式
*/
public class BeanContainerLazy {

// 防止指令重排序
private volatile static BeanContainerLazy instance;

private BeanContainerLazy(){}

public static BeanContainerLazy getInstance()
{
// 双重检测
if(instance == null)
{
synchronized (BeanContainerLazy.class)
{
if(instance == null)
{
instance = new BeanContainerLazy();
}
}
}

return instance;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonTest {

public static void main(String[] args) {

Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("Thread 1 get instance: " + BeanContainerLazy.getInstance());
}
};

Thread thread2 = new Thread(){
@Override
public void run() {
System.out.println("Thread 2 get instance: " + BeanContainerLazy.getInstance());
}
};

thread2.start();
thread1.start();
}
}

结果

懒汉模式同样保证了实例在运行时唯一

image.png

单例的安全性

尽管单例模式已经将构造方法设置为 private 但是依然存在让单例不唯一的手段。

  • 反射攻击
  • 序列化攻击

反射攻击

实现:通过反射创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.lang.reflect.Constructor;

public class SingletonTest {

public static void main(String[] args) {

Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("Thread 1 get instance: " + BeanContainerLazy.getInstance());
}
};

Thread thread2 = new Thread(){
@Override
public void run() {
System.out.println("Thread 2 get instance: " + BeanContainerLazy.getInstance());
}
};

// 反射创建懒汉单例模式
Thread thread3 = new Thread(){
@Override
public void run() {
Class lazyClazz = BeanContainerLazy.class;
Constructor constructor = null;
try {
constructor = lazyClazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("Thread 3 get instance: " + constructor.newInstance());
} catch (Exception e) {
e.printStackTrace();
}

}
};

thread2.start();
thread1.start();
thread3.start();
}
}

结果:出现了地址不相同的实例,表明通过反射可以破坏单例模式的唯一性

image.png

序列化攻击

懒汉式单,实现序列化接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.Serializable;

/**
* 懒汉单例模式
* 实现序列化接口
*
*/
public class BeanContainerLazySel implements Serializable {

// 防止指令重排序
private volatile static BeanContainerLazySel instance;

private BeanContainerLazySel(){}

public static BeanContainerLazySel getInstance()
{
// 双重检测
if(instance == null)
{
synchronized (BeanContainerLazySel.class)
{
if(instance == null)
{
instance = new BeanContainerLazySel();
}
}
}

return instance;
}
}

测试序列化攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.*;

public class SingletonTest {

/**
* 序列化
* @param instance
*/
private static void setObject(BeanContainerLazySel instance) throws IOException {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("instance"));
outputStream.writeObject(instance);
outputStream.close();
}

/**
* 反序列化
* @return
*/
private static BeanContainerLazySel getObject() throws IOException, ClassNotFoundException {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("instance"));
BeanContainerLazySel object = (BeanContainerLazySel)inputStream.readObject();
inputStream.close();

return object;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {

BeanContainerLazySel instance = BeanContainerLazySel.getInstance();
System.out.println(instance);
setObject(instance);

BeanContainerLazySel object = getObject();
System.out.println(object);


}
}

结果:在懒汉式单例下,通过序列化来获取单例,可以看到地址不一致。

image.png

最佳实践

抵御反射攻击

使用枚举类型

把懒汉式单例存放到枚举类型中,可防止反射和序列化攻击

以懒汉式单例为例子:

实现(枚举):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 懒汉单例模式
* 实现序列化接口
* 单例存放在枚举类型
*/
public class EnumBeanContainerStavingSel implements Serializable {

private EnumBeanContainerStavingSel(){}

public static EnumBeanContainerStavingSel getInstance()
{
return ContainerHolder.HOLDER.instance;
}

private enum ContainerHolder
{
HOLDER;
private EnumBeanContainerStavingSel instance;

ContainerHolder()
{
instance = new EnumBeanContainerStavingSel();
}
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.coding.singleton;

import java.io.*;
import java.lang.reflect.Constructor;

public class SingletonTest {

/**
* 序列化
* @param instance
*/
private static void setObject(EnumBeanContainerStavingSel instance) throws IOException {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("instance"));
outputStream.writeObject(instance);
outputStream.close();
}

/**
* 反序列化
* @return
*/
private static EnumBeanContainerStavingSel getObject() throws IOException, ClassNotFoundException {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("instance"));
EnumBeanContainerStavingSel object = (EnumBeanContainerStavingSel)inputStream.readObject();
inputStream.close();

return object;
}

public static void main(String[] args) {

Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("Thread 1 get instance: " + EnumBeanContainerStavingSel.getInstance());
}
};

// 反射攻击
Thread thread2 = new Thread(){
@Override
public void run() {

try {
Constructor constructor = EnumBeanContainerStavingSel.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumBeanContainerStavingSel enumBeanContainerStavingSel = (EnumBeanContainerStavingSel)constructor.newInstance();
System.out.println("Thread 2 get instance: " + enumBeanContainerStavingSel.getInstance());
} catch (Exception e) {
e.printStackTrace();
}

}
};

// 序列化攻击
Thread thread3 = new Thread(){
@Override
public void run() {

EnumBeanContainerStavingSel instance = EnumBeanContainerStavingSel.getInstance();

try {
// 把对象写入到磁盘
setObject(instance);

// 从磁盘中读取对象,并观察是否与运行中的实例相同
EnumBeanContainerStavingSel object = getObject();
System.out.println("Thread 3 get instance: " + object);
} catch (Exception e) {
e.printStackTrace();
}

}
};

thread2.start();
thread1.start();
thread3.start();
}
}

结果

可以看到放在枚举中的实:

  1. 反射的方式读取都是同一个类。
  2. 序列化方式读取,却不是同一个类,也就是说枚举类型能够抵御反射攻击,却不能抵御序列化攻击

image.png

抵御序列化攻击

在类中定义 readResolve 的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.Serializable;

/**
* 懒汉单例模式
* 实现序列化接口
* 单例存放在枚举类型
*/
public class EnumBeanContainerStavingSel implements Serializable {

private EnumBeanContainerStavingSel(){}

public static EnumBeanContainerStavingSel getInstance()
{
return ContainerHolder.HOLDER.instance;
}

private enum ContainerHolder
{
HOLDER;
private EnumBeanContainerStavingSel instance;

ContainerHolder()
{
instance = new EnumBeanContainerStavingSel();
}
}

private Object readResolve()
{
if(getInstance() == null)
return new EnumBeanContainerStavingSel();
else
return ContainerHolder.HOLDER.instance;
}

}

使用同样的测试用例进行测试,结果如下

image.png

结论

  1. 枚举类型实际上是 static 的代码块,会在类加载的时候执行,不能被反射创建。枚举类型中的实例是线程安全的。
  2. 在序列化对象的时候,如果要保证单例,则需要实现 readResolve() 方法保证对象唯一。
0%